<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.

Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!-- Author:  attila@google.com (Attila Bodis) -->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title>Closure Unit Tests - goog.ui.Tooltip</title>
  <script src="../base.js"></script>
  <script>
    goog.require('goog.dom');
    goog.require('goog.events');
    goog.require('goog.events.Event');
    goog.require('goog.events.EventHandler');
    goog.require('goog.events.EventType');
    goog.require('goog.positioning.AbsolutePosition');
    goog.require('goog.style');
    goog.require('goog.ui.Tooltip');
    goog.require('goog.testing.MockClock');
    goog.require('goog.testing.TestQueue');
    goog.require('goog.testing.events');
    goog.require('goog.testing.jsunit');
    goog.require('goog.userAgent');
  </script>
</head>
<body>
  <iframe id="testframe" style="width: 200px; height: 200px;"
          src="blank_test_helper.html">
  </iframe>
  <script>
    /**
     * A subclass of Tooltip that overrides {@code getPositioningStrategy}
     * for testing purposes.
     */
    function TestTooltip(el, text, dom) {
      goog.ui.Tooltip.call(this, el, text, dom);
    };
    goog.inherits(TestTooltip, goog.ui.Tooltip);


    /** @override */
    TestTooltip.prototype.getPositioningStrategy = function() {
      return new goog.positioning.AbsolutePosition(13, 17);
    };


    var tt, clock, handler, eventQueue, dom;

    // Allow positions to be off by one in gecko as it reports scrolling
    // offsets in steps of 2.
    var ALLOWED_OFFSET = goog.userAgent.GECKO ? 1 : 0;

    function setUp() {
      // We get access denied error when accessing the iframe in IE on the farm
      // as IE doesn't have the same window size issues as firefox on the farm
      // we bypass the iframe and use the current document instead.
      if (goog.userAgent.IE) {
        dom = goog.dom.getDomHelper(document);
      } else {
        var frame = document.getElementById('testframe');
        var doc = goog.dom.getFrameContentDocument(frame);
        dom = goog.dom.getDomHelper(doc);
      }

      // Host elements in fixed size iframe to avoid window size problems when
      // running under Selenium.
      dom.getDocument().body.innerHTML =
          '<p id="notpopup">Content</p>' +
          '<p id="hovertarget">Hover Here For Popup</p>' +
          '<p id="second">Secondary target</p>';

      tt = new goog.ui.Tooltip(undefined, undefined, dom);
      tt.setElement(dom.createDom('div', {id: 'popup',
                                          style: 'visibility:hidden'},
                                  'Hello'));
      clock = new goog.testing.MockClock(true);
      eventQueue = new goog.testing.TestQueue();
      handler = new goog.events.EventHandler(eventQueue);
      handler.listen(tt, goog.ui.PopupBase.EventType.SHOW, eventQueue.enqueue);
      handler.listen(tt, goog.ui.PopupBase.EventType.HIDE, eventQueue.enqueue);
    }

    function tearDown() {
      // tooltip needs to be hidden as well as disposed of so that it doesn't
      // leave global state hanging around to trip up other tests.
      tt.onHide_();
      tt.dispose();
      clock.uninstall();
      handler.removeAll();
    }

    function testConstructor() {
      var element = tt.getElement();
      assertNotNull('Tooltip should have non-null element', element);
      assertEquals('Tooltip element should be the DIV we created',
          dom.getElement('popup'), element);
      assertEquals('Tooltip element should be a child of the document body',
          dom.getDocument().body, element.parentNode);
    }

    function testTooltipShowsAndHides() {
      var hoverTarget = dom.getElement('hovertarget');
      var elsewhere = dom.getElement('notpopup');
      var element = tt.getElement();
      var position = new goog.math.Coordinate(5, 5);
      assertNotNull('Tooltip should have non-null element', element);
      assertEquals('Initial state should be inactive',
                   goog.ui.Tooltip.State.INACTIVE, tt.getState());
      tt.attach(hoverTarget);
      tt.setShowDelayMs(100);
      tt.setHideDelayMs(50);
      goog.testing.events.fireMouseOverEvent(hoverTarget, elsewhere, position);
      assertEquals(goog.ui.Tooltip.State.WAITING_TO_SHOW, tt.getState());
      clock.tick(101);
      assertEquals('visible', tt.getElement().style.visibility);
      assertEquals('tooltip y position (10px margin below the cursor)', '15px',
          tt.getElement().style.top);
      assertEquals(goog.ui.Tooltip.State.SHOWING, tt.getState());
      assertEquals(goog.ui.PopupBase.EventType.SHOW, eventQueue.dequeue().type);
      assertTrue(eventQueue.isEmpty());

      goog.testing.events.fireMouseOutEvent(hoverTarget, elsewhere);
      assertEquals(goog.ui.Tooltip.State.WAITING_TO_HIDE, tt.getState());
      clock.tick(51);
      assertEquals('hidden', tt.getElement().style.visibility);
      assertEquals(goog.ui.Tooltip.State.INACTIVE, tt.getState());
      assertEquals(goog.ui.PopupBase.EventType.HIDE, eventQueue.dequeue().type);
      assertTrue(eventQueue.isEmpty());
    }

    function testMultipleTargets() {
      var firstTarget = dom.getElement('hovertarget');
      var secondTarget = dom.getElement('second');
      var elsewhere = dom.getElement('notpopup');
      var element = tt.getElement();

      tt.attach(firstTarget);
      tt.attach(secondTarget);
      tt.setShowDelayMs(100);
      tt.setHideDelayMs(50);

      // Move over first target
      goog.testing.events.fireMouseOverEvent(firstTarget, elsewhere);
      clock.tick(101);
      assertEquals(goog.ui.PopupBase.EventType.SHOW, eventQueue.dequeue().type);
      assertTrue(eventQueue.isEmpty());

      // Move from first to second
      goog.testing.events.fireMouseOutEvent(firstTarget, secondTarget);
      goog.testing.events.fireMouseOverEvent(secondTarget, firstTarget);
      assertEquals(goog.ui.Tooltip.State.UPDATING,  tt.getState());
      assertTrue(eventQueue.isEmpty());

      // Move from second to element (before second shows)
      goog.testing.events.fireMouseOutEvent(secondTarget, element);
      goog.testing.events.fireMouseOverEvent(element, secondTarget);
      assertEquals(goog.ui.Tooltip.State.SHOWING, tt.getState());
      assertTrue(eventQueue.isEmpty());

      // Move from element to second, and let it show
      goog.testing.events.fireMouseOutEvent(element, secondTarget);
      goog.testing.events.fireMouseOverEvent(secondTarget, element);
      assertEquals(goog.ui.Tooltip.State.UPDATING, tt.getState());
      clock.tick(101);
      assertEquals(goog.ui.Tooltip.State.SHOWING, tt.getState());
      assertEquals('Anchor should be second target', secondTarget, tt.anchor);
      assertEquals(goog.ui.PopupBase.EventType.HIDE, eventQueue.dequeue().type);
      assertEquals(goog.ui.PopupBase.EventType.SHOW, eventQueue.dequeue().type);
      assertTrue(eventQueue.isEmpty());

      // Move from second to first and then off without first showing
      goog.testing.events.fireMouseOutEvent(secondTarget, firstTarget);
      goog.testing.events.fireMouseOverEvent(firstTarget, secondTarget);
      assertEquals(goog.ui.Tooltip.State.UPDATING, tt.getState());
      goog.testing.events.fireMouseOutEvent(firstTarget, elsewhere);
      assertEquals(goog.ui.Tooltip.State.WAITING_TO_HIDE, tt.getState());
      clock.tick(51);
      assertEquals('hidden', tt.getElement().style.visibility);
      assertEquals(goog.ui.Tooltip.State.INACTIVE, tt.getState());
      assertEquals(goog.ui.PopupBase.EventType.HIDE, eventQueue.dequeue().type);
      assertTrue(eventQueue.isEmpty());
      clock.tick(200);

      // Move from element to second, but detach second before it shows.
      goog.testing.events.fireMouseOutEvent(element, secondTarget);
      goog.testing.events.fireMouseOverEvent(secondTarget, element);
      assertEquals(goog.ui.Tooltip.State.WAITING_TO_SHOW, tt.getState());
      tt.detach(secondTarget);
      clock.tick(200);
      assertEquals(goog.ui.Tooltip.State.INACTIVE, tt.getState());
      assertEquals('Anchor should be second target', secondTarget, tt.anchor);
      assertTrue(eventQueue.isEmpty());
    }

    function testRequireInteraction() {
      var hoverTarget = dom.getElement('hovertarget');
      var elsewhere = dom.getElement('notpopup');

      tt.attach(hoverTarget);
      tt.setShowDelayMs(100);
      tt.setHideDelayMs(50);
      tt.setRequireInteraction(true);

      goog.testing.events.fireMouseOverEvent(hoverTarget, elsewhere);
      clock.tick(101);
      assertEquals(
          'Tooltip should not show without mouse move event',
          'hidden', tt.getElement().style.visibility);
      goog.testing.events.fireMouseMoveEvent(hoverTarget);
      goog.testing.events.fireMouseOverEvent(hoverTarget, elsewhere);
      clock.tick(101);
      assertEquals(
          'Tooltip should show because we had mouse move event',
          'visible', tt.getElement().style.visibility);

      goog.testing.events.fireMouseOutEvent(hoverTarget, elsewhere);
      clock.tick(51);
      assertEquals('hidden', tt.getElement().style.visibility);
      goog.testing.events.fireBrowserEvent(new goog.events.Event(
          goog.events.EventType.FOCUS, hoverTarget));
      clock.tick(101);
      assertEquals(
          'Tooltip should show because we had focus event',
          'visible', tt.getElement().style.visibility);
      goog.testing.events.fireBrowserEvent(new goog.events.Event(
          goog.events.EventType.BLUR, hoverTarget));
      clock.tick(51);
      assertEquals('hidden', tt.getElement().style.visibility);

      goog.testing.events.fireMouseMoveEvent(hoverTarget);
      goog.testing.events.fireMouseOverEvent(hoverTarget, elsewhere);
      goog.testing.events.fireMouseOutEvent(hoverTarget, elsewhere);
      goog.testing.events.fireMouseOverEvent(hoverTarget, elsewhere);
      clock.tick(101);
      assertEquals(
          'A cancelled trigger should also cancel the seen interaction',
          'hidden', tt.getElement().style.visibility);
    }

    function testDispose() {
      var element = tt.getElement();
      tt.dispose();
      assertTrue('Tooltip should have been disposed of', tt.isDisposed());
      assertNull('Tooltip element reference should have been nulled out',
          tt.getElement());
      assertNotEquals('Tooltip element should not be a child of the body',
          document.body, element.parentNode);
    }

    function testNested() {
      var ttNested;
      tt.getElement().appendChild(dom.createDom(
          'span', {id: 'nested'}, 'Goodbye'));
      ttNested = new goog.ui.Tooltip(undefined, undefined, dom);
      ttNested.setElement(dom.createDom('div', {id: 'nestedPopup'}, 'hi'));
      tt.setShowDelayMs(100);
      tt.setHideDelayMs(50);
      ttNested.setShowDelayMs(75);
      ttNested.setHideDelayMs(25);
      var nestedAnchor = dom.getElement('nested');
      var hoverTarget = dom.getElement('hovertarget');
      var outerTooltip = dom.getElement('popup');
      var innerTooltip = dom.getElement('nestedPopup');
      var elsewhere = dom.getElement('notpopup');

      ttNested.attach(nestedAnchor);
      tt.attach(hoverTarget);

      // Test mouse into, out of nested tooltip
      goog.testing.events.fireMouseOverEvent(hoverTarget, elsewhere);
      clock.tick(101);
      goog.testing.events.fireMouseOutEvent(hoverTarget, outerTooltip);
      goog.testing.events.fireMouseOverEvent(outerTooltip, hoverTarget);
      clock.tick(51);
      assertEquals('visible', tt.getElement().style.visibility);
      goog.testing.events.fireMouseOutEvent(outerTooltip, nestedAnchor);
      goog.testing.events.fireMouseOverEvent(nestedAnchor, outerTooltip);
      clock.tick(76);
      assertEquals('visible', tt.getElement().style.visibility);
      assertEquals('visible', ttNested.getElement().style.visibility);
      goog.testing.events.fireMouseOutEvent(nestedAnchor, outerTooltip);
      goog.testing.events.fireMouseOverEvent(outerTooltip, nestedAnchor);
      clock.tick(100);
      assertEquals('visible', tt.getElement().style.visibility);
      assertEquals('hidden', ttNested.getElement().style.visibility);

      // Go back in nested tooltip and then out through tooltip element.
      goog.testing.events.fireMouseOutEvent(outerTooltip, nestedAnchor);
      goog.testing.events.fireMouseOverEvent(nestedAnchor, outerTooltip);
      clock.tick(76);
      goog.testing.events.fireMouseOutEvent(nestedAnchor, innerTooltip);
      goog.testing.events.fireMouseOverEvent(innerTooltip, nestedAnchor);
      clock.tick(15);
      assertEquals('visible', tt.getElement().style.visibility);
      assertEquals('visible', ttNested.getElement().style.visibility);
      goog.testing.events.fireMouseOutEvent(innerTooltip, elsewhere);
      clock.tick(26);
      assertEquals('hidden', ttNested.getElement().style.visibility);
      clock.tick(51);
      assertEquals('hidden', tt.getElement().style.visibility);

      // Test with focus
      goog.testing.events.fireBrowserEvent(new goog.events.Event(
          goog.events.EventType.FOCUS, hoverTarget));
      clock.tick(101);
      goog.testing.events.fireBrowserEvent(new goog.events.Event(
          goog.events.EventType.BLUR, hoverTarget));
      goog.testing.events.fireBrowserEvent(new goog.events.Event(
          goog.events.EventType.FOCUS, nestedAnchor));
      clock.tick(76);
      assertEquals('visible', tt.getElement().style.visibility);
      assertEquals('visible', ttNested.getElement().style.visibility);
      goog.testing.events.fireBrowserEvent(new goog.events.Event(
          goog.events.EventType.BLUR, nestedAnchor));
      goog.testing.events.fireBrowserEvent(new goog.events.Event(
          goog.events.EventType.FOCUS, hoverTarget));
      clock.tick(26);
      assertEquals('visible', tt.getElement().style.visibility);
      assertEquals('hidden', ttNested.getElement().style.visibility);

      ttNested.onHide_();
      ttNested.dispose();
    }

    function testPosition() {
      dom.getDocument().body.style.paddingBottom = '150%'; // force scrollbar
      var scrollEl = dom.getDocumentScrollElement();

      var anchor = dom.getElement('hovertarget');
      var tooltip = new goog.ui.Tooltip(anchor, 'foo');
      tooltip.getElement().style.position = 'absolute';

      tooltip.cursorPosition.x = 100;
      tooltip.cursorPosition.y = 100;
      tooltip.showForElement(anchor);

      assertEquals('Tooltip should be at cursor position',
        '(110, 110)', // (100, 100) + padding (10, 10)
        goog.style.getPageOffset(tooltip.getElement()).toString());

      scrollEl.scrollTop = 50;

      var offset = goog.style.getPageOffset(tooltip.getElement());
      assertTrue('Tooltip should be at cursor position when scrolled',
        Math.abs(offset.x - 110) <= ALLOWED_OFFSET); // 100 + padding 10
      assertTrue('Tooltip should be at cursor position when scrolled',
        Math.abs(offset.y - 110) <= ALLOWED_OFFSET); // 100 + padding 10

      tooltip.dispose();
      dom.getDocument().body.style.paddingTop = '';
      scrollEl.scrollTop = 0;
    }

    function testPositionOverride() {
      var anchor = dom.getElement('hovertarget');
      var tooltip = new TestTooltip(anchor, 'foo', dom);

      tooltip.showForElement(anchor);

      assertEquals('Tooltip should be at absolute position', '(13, 17)',
          goog.style.getPageOffset(tooltip.getElement()).toString());
      tooltip.dispose();
    }
  </script>
</body>
</html>
